﻿using EnvDTE;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;

namespace WpfPropertyEditorExtension
{
    public class XamlEditorService
    {
        public Document GetActiveXamlDocument()
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            var dte = (DTE)Package.GetGlobalService(typeof(SDTE));
            var doc = dte?.ActiveDocument;
            if (doc == null) return null;
            if (!doc.Name.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase)) return null;
            return doc;
        }

        public (int line, int column)? GetCaretLineColumn(Document xamlDoc, out string fullText)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            fullText = null;
            var td = xamlDoc.Object("TextDocument") as TextDocument;
            if (td == null) return null;

            var epStart = td.StartPoint.CreateEditPoint();
            var epEnd = td.EndPoint.CreateEditPoint();
            fullText = epStart.GetText(epEnd);

            var pt = td.Selection.ActivePoint;
            return (pt.Line, pt.DisplayColumn);
        }

        public XElement FindElementAtLine(string xamlText, int caretLine)
        {
            // parse with line info
            XDocument doc;
            try
            {
                var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
                using (var reader = XmlReader.Create(new System.IO.StringReader(xamlText), settings))
                {
                    doc = XDocument.Load(reader, LoadOptions.SetLineInfo);
                }
            }
            catch { return null; }

            var els = doc.Descendants().OfType<XElement>()
                .Where(e => (e as IXmlLineInfo)?.HasLineInfo() == true)
                .Select(e => new { E = e, Line = ((IXmlLineInfo)e).LineNumber })
                .Where(x => x.Line <= caretLine)
                .OrderBy(x => x.Line)
                .ToList();
            return els.LastOrDefault()?.E;
        }

        public int GetLine(XElement el) => (el as IXmlLineInfo)?.LineNumber ?? 0;

        /// <summary>
        /// Updates either start tag (attributes) or inner text content, by replacing only the start-tag span or content span in the active XAML document.
        /// This is a pragmatic approach: we locate the start-tag bounds around the element's starting line and replace that section.
        /// </summary>
        public void UpdateStartTagOrContent(XElement element, bool updateContent)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            var doc = GetActiveXamlDocument();
            if (doc == null) return;

            var td = doc.Object("TextDocument") as TextDocument;
            if (td == null) return;
            var epStart = td.StartPoint.CreateEditPoint();
            var epEnd = td.EndPoint.CreateEditPoint();
            string text = epStart.GetText(epEnd);

            // Serialize a new start tag
            string startTag = BuildStartTag(element);

            int startLine = GetLine(element);
            var (startIdx, endIdx, selfClosing) = FindStartTagSpan(text, startLine);
            if (startIdx < 0 || endIdx <= startIdx) return;

            if (!updateContent)
            {
                // Replace only start tag
                ReplaceSpan(td, startIdx, endIdx, startTag);
            }
            else
            {
                // Replace inner text if element is not self-closing and has no child elements
                if (selfClosing) return;

                // Find content bounds: after '>' of start tag until before '</'
                int contentStart = endIdx;
                int closeIdx = FindClosingTagOfSameElement(text, endIdx, element.Name);
                if (closeIdx <= contentStart) return;

                // Replace content
                ReplaceSpan(td, contentStart, closeIdx, element.HasElements ? "" : element.Value);
            }

            doc.Save();
        }

        private static void ReplaceSpan(TextDocument td, int startIdx, int endIdx, string newText)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            var epStart = td.StartPoint.CreateEditPoint();
            epStart.CharRight(startIdx);
            var epEnd = td.StartPoint.CreateEditPoint();
            epEnd.CharRight(endIdx);
            epStart.Delete(epEnd);
            epStart.Insert(newText);
        }

        private static string BuildStartTag(XElement el)
        {
            // Preserve element name and attributes; do not self-close here (designer will handle formatting)
            var ns = el.Name;
            var sb = new System.Text.StringBuilder();
            sb.Append("<").Append(ns.ToString());
            foreach (var a in el.Attributes())
            {
                sb.Append(" ").Append(a.Name.ToString()).Append("=")
                  .Append(EscapeAttribute(a.Value)).Append("");
            }
            // Decide self-closing based on element content
            if (el.HasElements || !string.IsNullOrEmpty(el.Value))
                sb.Append(">");
            else
                sb.Append(" />");
            return sb.ToString();
        }

        private static string EscapeAttribute(string value)
            => value?.Replace("&", "&amp;").Replace("", "&quot;").Replace("<", "&lt;").Replace(">", "&gt;") ?? "";

        private static (int start, int end, bool selfClosing) FindStartTagSpan(string text, int approxLine)
        {
            // naive line index
            int idx = 0; int line = 1;
            while (idx < text.Length && line < approxLine)
            {
                if (text[idx] == '\n') line++;
                idx++;
            }
            // Backtrack to nearest '<'
            int start = idx;
            while (start > 0 && text[start] != '<') start--;
            if (start < 0 || text[start] != '<') return (-1, -1, false);

            // Scan until '>' respecting quotes
            bool inQuote = false; char quote = '\0';
            int i = start;
            while (i < text.Length)
            {
                char c = text[i];
                if (!inQuote && (c == '"' || c == '\'')) { inQuote = true; quote = c; }
                else if (inQuote && c == quote) { inQuote = false; quote = '\0'; }
                else if (!inQuote && c == '>')
                {
                    bool selfClosing = (i - 1 >= 0 && text[i - 1] == '/');
                    return (start, i + 1, selfClosing);
                }
                i++;
            }
            return (-1, -1, false);
        }

        private static int FindClosingTagOfSameElement(string text, int fromIndex, XName name)
        {
            // naive: find next occurrence of "</name"
            string token = "</" + name.ToString();
            int idx = text.IndexOf(token, fromIndex, StringComparison.Ordinal);
            return idx;
        }
    }
}
